JavaScript学习笔记:class语法糖

berry-close-up-delicious
在前面一篇原型与继承的学习笔记中,最后使用基于原型的范式实现了继承。而 ES6 引入的 class 关键字则提供了更加漂亮的语法,与 Java 等语言有点类似。还是以 Rabbit 继承 Animal 为例,使用 class 关键字的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Animal {
constructor(name, speed) {
this.name = name;
this.speed = speed;
}

// 静态方法是从属于 class 本身的,而非其原型。静态属性同理。
// 等同于:
// Animal.compare = function(animalA, animalB) { ... }
static compare(animalA, animalB) {
return animalA.speed - animalB.speed;
}

// 成员方法是从属于 class 的原型的
// 等同于:
// Animal.prototype.run = function(speed) { ... }
run(speed) {
this.speed += speed;
console.log(`${this.name} run with speed ${this.speed}`);
}

stop() {
this.speed = 0;
console.log(`${this.name} stopped`);
}
}

class Rabbit extends Animal {
constructor(name, speed, earLength) {
super(name, speed); // 必须使用 super 调用父级构造器,然后才能访问 this
this.earLength = earLength;
}

hide() {
console.log(`${this.name} hide`);
}

stop() {
// 通过 super.method() 可以调用父类的方法
super.stop();
this.hide();
}
}

console.log(typeof Animal); // function
console.log(typeof Rabbit); // function

需要注意的几点:

  • class 只是语法糖,在语言层面并未引入新的基本单元,从 typeof Animal 的结果 function 可以看到,本质上依然是函数。
  • 子类的构造器 constructor 在访问 this 之前必须先调用父类构造器,即调用 super(...)
  • 为了更多地复用代码,在子类中可以使用 super.method() 调用父类的方法。
  • 声明的成员方法 runstophide 都是定义在其原型上。以 stop 方法为例,等同于: Animal.prototype = function () {...}
  • 声明的静态方法则是定义在 class 函数本身上,而非其原型上。
  • extends 关键字实际上做了以下工作:
    • Rabbit.prototype 的原型设为 Animal.prototype,使得 Rabbit 的实例可以继承 Animal 的实例属性
    • Rabbit 的原型设为 Animal,使得 Rabbit 本身可以继承 Animal 的静态属性
  • class 内声明的方法之间没有逗号 ,

静态属性与实例属性

在 JavaScript 中,结合 class 关键字,用类似 Java 中面向对象的思想来理解静态属性与实例属性。静态属性是挂载在 class 本身上的,而实例属性是挂载在原型 prototype 上的。我们以内置的 ObjectDate 这两个对象为例来说明,如下图所示:

从上图可以清晰的看到:ObjectDate 两者本身是不存在继承关系的,它们只是通过彼此的原型实现继承关系,仅此而已。

Object 的静态属性

  • Object.name
  • Object.length
  • Object.prototype
  • Object.assign
  • Object.getOwnPropertyDescriptor
  • Object.getOwnPropertyDescriptors
  • Object.getOwnPropertyNames
  • Object.getOwnPropertySymbols
  • Object.is
  • Object.preventExtensions
  • Object.seal
  • Object.create
  • Object.defineProperties
  • Object.defineProperty
  • Object.freeze
  • Object.getPrototypeOf
  • Object.setPrototypeOf
  • Object.isExtensible
  • Object.isFrozen
  • Object.isSealed
  • Object.keys
  • Object.entries
  • Object.values

Object 的实例属性

  • constructor
  • __defineGetter__
  • __defineSetter__
  • hasOwnProperty
  • __lookupGetter__
  • __lookupSetter__
  • isPrototypeOf
  • propertyIsEnumerable
  • toString
  • valueOf
  • __proto__
  • toLocaleString

Date 的静态属性

  • Date.length
  • Date.name
  • Date.prototype
  • Date.now
  • Date.parse
  • Date.UTC

Date 的实例属性

  • constructor
  • toString
  • toDateString
  • toTimeString
  • toISOString
  • toUTCString
  • toGMTString
  • getDate
  • setDate
  • getDay
  • getFullYear
  • setFullYear
  • getHours
  • setHours
  • getMilliseconds
  • setMilliseconds
  • getMinutes
  • setMinutes
  • getMonth
  • setMonth
  • getSeconds
  • setSeconds
  • getTime
  • setTime
  • getTimezoneOffset
  • getUTCDate
  • setUTCDate
  • getUTCDay
  • getUTCFullYear
  • setUTCFullYear
  • getUTCHours
  • setUTCHours
  • getUTCMilliseconds
  • setUTCMilliseconds
  • getUTCMinutes
  • setUTCMinutes
  • getUTCMonth
  • setUTCMonth
  • getUTCSeconds
  • setUTCSeconds
  • valueOf
  • getYear
  • setYear
  • toJSON
  • toLocaleString
  • toLocaleDateString
  • toLocaleTimeString.
  • Symbol.toPrimitive.

instanceof 运算符

instanceof 运算符的语法如下:

1
obj instanceof Class;

这一运算符的本质其实是判断 Class.prototype 是否在 obj 对象的原型链上。除此之外,对于那些部署了 Symbol.hasInstance 静态方法的类,还要额外考虑。
obj instanceof Class 这一算法的工作过程大致如下:

  1. 如果某个类部署了 Symbol.hasInstance 静态方法,那么直接使用该方法。比如下面这样:
1
2
3
4
5
6
7
8
9
// assume anything that canEat is an animal
class Animal {
static [Symbol.hasInstance](obj) {
if (obj.canEat) return true;
}
}

let obj = { canEat: true };
alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj) is called
  1. 如果某个类没有部署 Symbol.hasInstance 静态方法(大多数类都没有部署)。那么开始检查这个类的原型链,看 Class.prototype 是否等于原型链上的某个原型,即做如下比较:
1
2
3
4
obj.__proto__ === Class.prototype
obj.__proto__.__proto__ === Class.prototype
obj.__proto__.__proto__.__proto__ === Class.prototype
...

还是以 Rabbit 继承 Animal 为例,来看看 instanceof 的工作过程:

1
2
3
4
5
6
7
class Animal {}
class Rabbit extends Animal {}

let rabbit = new Rabbit();
alert(rabbit instanceof Animal); // true
// rabbit.__proto__ === Rabbit.prototype
// rabbit.__proto__.__proto__ === Animal.prototype (match!)

上面的代码中,rabbit 实例的原型链是这样的:

1
rabbit -> Rabbit.prototpye -> Animal.prototype -> Object.prototype -> null

rabbit instanceof Animal 执行的操作是在上面的原型链中查找,看 Animal.prototype 是否出现在原型链中,如果是则返回 true,否则返回 false

类型检查

在 JavaScript 中,主要有 3 种方法可以用来做类型检查:

  • typeof 主要用于检查基本数据类型,返回一个说明了所属类型的字符串。比如:
1
alert(typeof 1); // number
  • {}.toString 用于检查基本数据类型,内置对象,以及部署了 Symbol.toStringTag 属性的对象。可以看做是增强版的 typeof 。示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 对于基本数据类型
let s = Object.prototype.toString;

alert(s.call(123)); // [object Number]
alert(s.call(null)); // [object Null]
alert(s.call(alert)); // [object Function]

// 对于部署了 Symbol.toStringTag 的对象
let user = {
[Symbol.toStringTag]: "User"
};

alert({}.toString.call(user)); // [object User]

// 对于特定环境的内置对象和类
alert(window[Symbol.toStringTag]); // window
alert(XMLHttpRequest.prototype[Symbol.toStringTag]); // XMLHttpRequest

alert({}.toString.call(window)); // [object Window]
alert({}.toString.call(new XMLHttpRequest())); // [object XMLHttpRequest]
  • instanceof 用于对检查对象数据类型,尤其用于检查对象继承关系。